/**
 * Embedded System Building Block (Clock/Calendar)
 *
 * @file CLK.C
 * @author Jean J. Labrosse
 */

#include <string.h>
#include "OS_CPU.H"
#include "OS_CFG.H"
#include "uCOS_II.H"
#include "clk.h"

extern char* itoa ( int num, char* str, int radix );
/**
 * LOCAL CONSTANTS
 */
#define  CLK_TS_BASE_YEAR    2000      /* Time stamps start year                                       */


/**
 * LOCAL VARIABLES
 */
static  OS_EVENT   *ClkSem;            /* Semaphore used to access the time of day clock               */
static  OS_EVENT   *ClkSemSec;         /* Counting semaphore used to keep track of seconds             */
static  OS_STK      ClkTaskStk[CLK_TASK_STK_SIZE];
static  INT16U       ClkTickCtr;      /* Counter used to keep track of system clock ticks             */
static  INT8U   ClkHr;
static  INT8U   ClkMin;
static  INT8U   ClkSec;               /* Counters for local TIME                                      */

#if      CLK_DATE_EN
static  INT8U   ClkDay;               /* Counters for local DATE                                      */
static  INT8U   ClkDOW;               /* Day of week (0 is Sunday)                                    */
static  INT8U   ClkMonth;
static  INT16U  ClkYear;
#endif

#if      CLK_TS_EN
static  TS      ClkTS;                /* Current TIME-STAMP                                           */
#endif


#if CLK_DATE_EN
static  char *ClkDOWTbl[] =            /* NAME FOR EACH DAY OF THE WEEK                                */
{
    "Sunday ",
    "Monday ",
    "Tuesday ",
    "Wednesday ",
    "Thursday ",
    "Friday ",
    "Saturday "
};
static  CLK_MONTH ClkMonthTbl[] =      /* MONTHS TABLE                                                 */
{
    {0,  "",           0},             /* Invalid month                                                */
    {31, "January ",   6},             /* January                                                      */
    {28, "February ",  2},             /* February (note leap years are handled by code)               */
    {31, "March ",     2},             /* March                                                        */
    {30, "April ",     5},             /* April                                                        */
    {31, "May ",       0},             /* May                                                          */
    {30, "June ",      3},             /* June                                                         */
    {31, "July ",      5},             /* July                                                         */
    {31, "August ",    1},             /* August                                                       */
    {30, "September ", 4},             /* September                                                    */
    {31, "October ",   6},             /* October                                                      */
    {30, "November ",  2},             /* November                                                     */
    {31, "December ",  4}              /* December                                                     */
};
#endif

/**
 * LOCAL FUNCTION PROTOTYPES
 */
void    ClkTask ( void *data );
static  BOOLEAN ClkUpdateTime ( void );

#if     CLK_DATE_EN
static  BOOLEAN       ClkIsLeapYear ( INT16U year );
static  void          ClkUpdateDate ( void );
static  void          ClkUpdateDOW ( void );
#endif

/**
 * Formats the current date into an ASCII string.
 * @para n     is the format type:
 *             1   will format the time as "MM-DD-YY"           (needs at least  9 characters)
 *             2   will format the time as "Day Month DD, YYYY" (needs at least 30 characters)
 *             3   will format the time as "YYYY-MM-DD"         (needs at least 11 characters)
 * @para s     is a pointer to the destination string.  The destination string must be large
 *             enough to hold the formatted date.
 *
 * @note       A 'switch' statement has been used to allow you to add your own date formats.  For
 *             example, you could display the date in French, Spanish, German etc. by assigning
 *             numbers for those types of conversions.
 *             - This function assumes that strcpy(), strcat() and itoa() are reentrant.
 */
#if  CLK_DATE_EN
void  ClkFormatDate ( INT8U n, char *s )
{
    INT8U   err;
    INT16U  year;
    char    str[5];


    OSSemPend ( ClkSem, 0, &err );               /* Gain exclusive access to time-of-day clock         */
    switch ( n )
    {
    case  1:
        strcpy ( s, "MM-DD-YY" );          /* Create the template for the selected format        */
        s[0] = ClkMonth / 10 + '0';        /* Convert DATE to ASCII                              */
        s[1] = ClkMonth % 10 + '0';
        s[3] = ClkDay   / 10 + '0';
        s[4] = ClkDay   % 10 + '0';
        year = ClkYear % 100;
        s[6] = year / 10 + '0';
        s[7] = year % 10 + '0';
        break;

    case  2:
        strcpy ( s, ClkDOWTbl[ClkDOW] );               /* Get the day of the week                */
        (void) strcat ( s, ClkMonthTbl[ClkMonth].MonthName ); /* Get name of month                      */
        if ( ClkDay < 10 )
        {
            str[0] = ClkDay + '0';
            str[1] = 0;
        }
        else
        {
            str[0] = ClkDay / 10 + '0';
            str[1] = ClkDay % 10 + '0';
            str[2] = 0;
        }
        (void) strcat ( s, str );
        (void) strcat ( s, ", " );
        (void) itoa ( ClkYear, str, 10 );
        (void) strcat ( s, str );
        break;

    case  3:
        strcpy ( s, "YYYY-MM-DD" );        /* Create the template for the selected format        */
        s[0] = year / 1000 + '0';
        year = year % 1000;
        s[1] = year /  100 + '0';
        year = year %  100;
        s[2] = year /   10 + '0';
        s[3] = year %   10 + '0';
        s[5] = ClkMonth / 10 + '0';        /* Convert DATE to ASCII                              */
        s[6] = ClkMonth % 10 + '0';
        s[8] = ClkDay   / 10 + '0';
        s[9] = ClkDay   % 10 + '0';
        break;

    default:
        strcpy ( s, "?" );
        break;
    }
    (void) OSSemPost ( ClkSem );                        /* Release access to clock                            */
}
#endif

/**
 * Formats the current time into an ASCII string.
 * @para n      is the format type:
 *              1   will format the time as "HH:MM:SS"     (24 Hour format)
 *                    (needs at least  9 characters)
 *              2   will format the time as "HH:MM:SS AM"  (With AM/PM indication)
 *                    (needs at least 13 characters)
 * @para s      is a pointer to the destination string.  The destination string must be large
 *                    enough to hold the formatted time.
 * @note        A 'switch' statement has been used to allow you to add your own time formats.
 *               - This function assumes that strcpy() is reentrant.
 */
void  ClkFormatTime ( INT8U n, char *s )
{
    INT8U err;
    INT8U hr;


    OSSemPend ( ClkSem, 0, &err );                    /* Gain exclusive access to time-of-day clock    */
    switch ( n )
    {
    case  1:
        strcpy ( s, "HH:MM:SS" );               /* Create the template for the selected format   */
        s[0] = ClkHr  / 10 + '0';               /* Convert TIME to ASCII                         */
        s[1] = ClkHr  % 10 + '0';
        s[3] = ClkMin / 10 + '0';
        s[4] = ClkMin % 10 + '0';
        s[6] = ClkSec / 10 + '0';
        s[7] = ClkSec % 10 + '0';
        break;

    case  2:
        strcpy ( s, "HH:MM:SS AM" );            /* Create the template for the selected format   */
        s[9] = ( ClkHr >= 12 ) ? 'P' : 'A';     /* Set AM or PM indicator                        */
        if ( ClkHr > 12 )                       /* Adjust time to be displayed                   */
        {
            hr   = ClkHr - 12;
        }
        else
        {
            hr = ClkHr;
        }
        s[0] = hr     / 10 + '0';               /* Convert TIME to ASCII                         */
        s[1] = hr     % 10 + '0';
        s[3] = ClkMin / 10 + '0';
        s[4] = ClkMin % 10 + '0';
        s[6] = ClkSec / 10 + '0';
        s[7] = ClkSec % 10 + '0';
        break;

    default:
        strcpy ( s, "?" );
        break;
    }
    (void) OSSemPost ( ClkSem );                             /* Release access to time-of-day clock           */
}

/**
 * This function converts a time-stamp to an ASCII string.
 * @para n         is the desired format number:
 *                  1 : "MM-DD-YY HH:MM:SS"         (needs at least 18 characters)
 *                  2 : "YYYY-MM-DD HH:MM:SS"       (needs at least 20 characters)
 * @para ts        is the time-stamp value to format
 * @para s         is the destination ASCII string
 *
 * @note        The time stamp is a 32 bit unsigned integer as follows:
 *
 *        Field: -------Year------ ---Month--- ------Day----- ----Hours----- ---Minutes--- --Seconds--
 *        Bit# : 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
 *
 *               - The year is based from CLK_TS_BASE_YEAR.  That is, if bits 31..26 contain 0 it really
 *                 means that the year is really CLK_TS_BASE_YEAR.  If bits 31..26 contain 13, the year
 *                 is CLK_TS_BASE_YEAR + 13.
 */
#if CLK_TS_EN && CLK_DATE_EN
void  ClkFormatTS ( INT8U n, TS ts, char *s )
{
    INT16U yr;
    INT8U month;
    INT8U day;
    INT8U hr;
    INT8U min;
    INT8U sec;

    yr    = CLK_TS_BASE_YEAR + (INT16U) ( ts >> 26 );     /* Unpack time-stamp                   */
    month = ( ts >> 22 ) & 0x0F;
    day   = ( ts >> 17 ) & 0x1F;
    hr    = ( ts >> 12 ) & 0x1F;
    min   = ( ts >>  6 ) & 0x3F;
    sec   = ( ts & 0x3F );
    switch ( n )
    {
    case  1:
        strcpy ( s, "MM-DD-YY HH:MM:SS" ); /* Create the template for the selected format        */
        yr    = yr % 100;
        s[ 0] = month / 10 + '0';          /* Convert DATE to ASCII                              */
        s[ 1] = month % 10 + '0';
        s[ 3] = day   / 10 + '0';
        s[ 4] = day   % 10 + '0';
        s[ 6] = yr    / 10 + '0';
        s[ 7] = yr    % 10 + '0';
        s[ 9] = hr    / 10 + '0';           /* Convert TIME to ASCII                             */
        s[10] = hr    % 10 + '0';
        s[12] = min   / 10 + '0';
        s[13] = min   % 10 + '0';
        s[15] = sec   / 10 + '0';
        s[16] = sec   % 10 + '0';
        break;

    case  2:
        strcpy ( s, "YYYY-MM-DD HH:MM:SS" ); /* Create the template for the selected format        */
        s[ 0] = yr    / 1000 + '0';        /* Convert DATE to ASCII                              */
        yr    = yr % 1000;
        s[ 1] = yr    /  100 + '0';
        yr    = yr %  100;
        s[ 2] = yr    /   10 + '0';
        s[ 3] = yr    %   10 + '0';
        s[ 5] = month / 10 + '0';
        s[ 6] = month % 10 + '0';
        s[ 8] = day   / 10 + '0';
        s[ 9] = day   % 10 + '0';
        s[11] = hr    / 10 + '0';           /* Convert TIME to ASCII                             */
        s[12] = hr    % 10 + '0';
        s[14] = min   / 10 + '0';
        s[15] = min   % 10 + '0';
        s[17] = sec   / 10 + '0';
        s[18] = sec   % 10 + '0';
        break;

    default:
        strcpy ( s, "?" );
        break;
    }
    return;
}
#endif

/**
 * This function is used to return a time-stamp to your application.  The format of the
 *               time-stamp is shown below:
 *        Field: -------Year------ ---Month--- ------Day----- ----Hours----- ---Minutes--- --Seconds--
 *        Bit# : 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0

 * @note       The year is based from CLK_TS_BASE_YEAR.  That is, if bits 31..26 contain 0 it really
 *             means that the year is really CLK_TS_BASE_YEAR.  If bits 31..26 contain 13, the year
 *             is CLK_TS_BASE_YEAR + 13.
 */
#if CLK_TS_EN && CLK_DATE_EN
TS  ClkGetTS ( void )
{
#if OS_CRITICAL_METHOD == 3u                           /* Allocate storage for CPU status register     */
    OS_CPU_SR  cpu_sr = 0u;
#endif
    TS ts;
    OS_ENTER_CRITICAL();
    ts = ClkTS;
    OS_EXIT_CRITICAL();
    return ( ts );
}
#endif

/**
 * This function initializes the time module.  The time of day clock task will be created
 *               by this function.
 */
void  ClkInit ( void )
{
    ClkSem     = OSSemCreate ( 1 );    /* Create time of day clock semaphore                           */
    ClkSemSec  = OSSemCreate ( 0 );    /* Create counting semaphore to signal the occurrence of 1 sec. */
    ClkTickCtr =    0;
    ClkSec     =    0;
    ClkMin     =    0;
    ClkHr      =    0;
#if CLK_DATE_EN
    ClkDay     =    1;
    ClkMonth   =    1;
    ClkYear    = 2013;
#endif
#if CLK_TS_EN && CLK_DATE_EN
    ClkTS      = ClkMakeTS ( ClkMonth, ClkDay, ClkYear, ClkHr, ClkMin, ClkSec );
#endif
    (void) OSTaskCreate ( ClkTask, ( void * ) 0, &ClkTaskStk[CLK_TASK_STK_SIZE - 1], CLK_TASK_PRIO );
}

/**
 * This function determines whether the 'year' passed as an argument is a leap year.
 * @para year    is the year to check for leap year.
 * @ret  TRUE    if 'year' is a leap year.
 *       FALSE   if 'year' is NOT a leap year.
 */
#if CLK_DATE_EN
static  BOOLEAN  ClkIsLeapYear ( INT16U year )
{
    if ( ! ( year % 4 ) && ( year % 100 ) || ! ( year % 400 ) )
    {
        return TRUE;
    }
    else
    {
        return ( FALSE );
    }
}
#endif

/**
 * This function maps a user specified date and time into a 32 bit variable called a time-stamp.
 *
 * @para month     is the desired month   (1..12)
 * @para day       is the desired day     (1..31)
 * @para year      is the desired year    (CLK_TS_BASE_YEAR .. CLK_TS_BASE_YEAR+63)
 * @para hr        is the desired hour    (0..23)
 * @para min       is the desired minutes (0..59)
 * @para sec       is the desired seconds (0..59)
 * @ret  A time-stamp based on the arguments passed to the function.
 *
 * The time stamp is formatted as follows using a 32 bit unsigned integer:
 *        Field: -------Year------ ---Month--- ------Day----- ----Hours----- ---Minutes--- --Seconds--
 *        Bit# : 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
 * The year is based from CLK_TS_BASE_YEAR.  That is, if bits 31..26 contain 0 it really
 * means that the year is really CLK_TS_BASE_YEAR.  If bits 31..26 contain 13, the year is
 * CLK_TS_BASE_YEAR + 13.
 */
#if CLK_TS_EN && CLK_DATE_EN
TS  ClkMakeTS ( INT8U month, INT8U day, INT16U yr, INT8U hr, INT8U min, INT8U sec )
{
    TS ts;


    yr -= CLK_TS_BASE_YEAR;
    ts  = ( ( INT32U ) yr << 26 ) | ( ( INT32U ) month << 22 ) | ( ( INT32U ) day << 17 );
    ts |= ( ( INT32U ) hr << 12 ) | ( ( INT32U ) min   <<  6 ) |  ( INT32U ) sec;
    return ( ts );
}
#endif

/**
 * Set the date of the time-of-day clock
 * @para month     is the desired month (1..12)
 * @para day       is the desired day   (1..31)
 * @para year      is the desired year  (CLK_TS_BASE_YEAR .. CLK_TS_BASE_YEAR+63)
 *
 * It is assumed that you are specifying a correct date (i.e. there is no range checking
 *               done by this function).
 */
#if  CLK_DATE_EN
void  ClkSetDate ( INT8U month, INT8U day, INT16U year )
{
    INT8U err;


    OSSemPend ( ClkSem, 0, &err );               /* Gain exclusive access to time-of-day clock         */
    ClkMonth = month;
    ClkDay   = day;
    ClkYear  = year;
    ClkUpdateDOW();                              /* Compute the day of the week (i.e. Sunday ...)      */
    (void) OSSemPost ( ClkSem );                        /* Release access to time-of-day clock                */
}
#endif

/**
 * Set the date and time of the time-of-day clock
 * @para month     is the desired month (1..12)
 * @para day       is the desired day   (1..31)
 * @para year      is the desired year    (2xxx)
 * @para hr        is the desired hour    (0..23)
 * @para min       is the desired minutes (0..59)
 * @para sec       is the desired seconds (0..59)
 * It is assumed that you are specifying a correct date (i.e. there is no range checking
 *               done by this function).
 */
#if  CLK_DATE_EN
void  ClkSetDateTime ( INT8U month, INT8U day, INT16U year, INT8U hr, INT8U min, INT8U sec )
{
    INT8U err;


    OSSemPend ( ClkSem, 0, &err );               /* Gain exclusive access to time-of-day clock         */
    ClkMonth = month;
    ClkDay   = day;
    ClkYear  = year;
    ClkHr    = hr;
    ClkMin   = min;
    ClkSec   = sec;
    ClkUpdateDOW();                              /* Compute the day of the week (i.e. Sunday ...)      */
    (void) OSSemPost ( ClkSem );                        /* Release access to time-of-day clock                */
}
#endif

/**
 * Set the time-of-day clock
 * @para hr        is the desired hour    (0..23)
 * @para min       is the desired minutes (0..59)
 * @para sec       is the desired seconds (0..59)
 * It is assumed that you are specifying a correct date (i.e. there is no range checking
 *               done by this function).
 */
void  ClkSetTime ( INT8U hr, INT8U min, INT8U sec )
{
#if OS_CRITICAL_METHOD == 3u                           /* Allocate storage for CPU status register     */
    OS_CPU_SR  cpu_sr = 0u;
#endif
    OS_ENTER_CRITICAL();                         /* Gain exclusive access to time-of-day clock         */
    ClkHr  = hr;
    ClkMin = min;
    ClkSec = sec;
    OS_EXIT_CRITICAL();                          /* Release access to time-of-day clock                */
}

/**
 * This function is called by the 'clock tick' ISR on every tick.  This function is thus
 *    responsible for counting the number of clock ticks per second.  When a second elapses,
 *    this function will signal the time-of-day clock task.
 *
 * CLK_DLY_TICKS must be set to the number of ticks to produce 1 second.
 *    This would typically correspond to OS_TICKS_PER_SEC if you use uC/OS-II.
 */
void  ClkSignalClk ( void )
{
    ClkTickCtr++;                           /* count the number of 'clock ticks' for one second        */
    if ( ClkTickCtr >= CLK_DLY_TICKS )
    {
        ClkTickCtr = 0;
        (void) OSSemPost ( ClkSemSec );            /* Signal that one second elapsed                          */
    }
}

/**
 * This task is created by ClkInit() and is responsible for updating the time and date.
 *               ClkTask() executes every second.
 *
 * CLK_DLY_TICKS must be set to produce 1 second delays.
 */
void  ClkTask ( void *data )
{
    INT8U err;


    data = data;                            /* Avoid compiler warning (uC/OS requirement)              */
    for ( ;; )
    {

#if CLK_USE_DLY
        (void) OSTimeDlyHMSM ( 0, 0, 1, 0 );       /* Delay for one second                                    */
#else
        OSSemPend ( ClkSemSec, 0, &err );   /* Wait for one second to elapse                           */
#endif

        OSSemPend ( ClkSem, 0, &err );      /* Gain exclusive access to time-of-day clock              */
        if ( ClkUpdateTime() == TRUE )      /* Update the TIME (i.e. HH:MM:SS)                         */
        {
#if CLK_DATE_EN
            ClkUpdateDate();                /* And date if a new day (i.e. MM-DD-YY)                   */
#endif
        }
#if CLK_TS_EN && CLK_DATE_EN
        ClkTS = ClkMakeTS ( ClkMonth, ClkDay, ClkYear, ClkHr, ClkMin, ClkSec );
#endif
        (void) OSSemPost ( ClkSem );               /* Release access to time-of-day clock                     */
    }
}

/**
 * This function is called to update the date (i.e. month, day and year)
 * This function updates ClkDay, ClkMonth, ClkYear and ClkDOW.
 */
#if CLK_DATE_EN
static  void  ClkUpdateDate ( void )
{
    BOOLEAN newmonth = TRUE;
    if ( ClkDay >= ClkMonthTbl[ClkMonth].MonthDays )  /* Last day of the month?                        */
    {
        if ( ClkMonth == 2 )                          /* Is this February?                             */
        {
            if ( ClkIsLeapYear ( ClkYear ) == TRUE )  /* Yes, Is this a leap year?                     */
            {
                if ( ClkDay >= 29 )                   /* Yes, Last day in february?                    */
                {
                    ClkDay = 1;                       /* Yes, Set to 1st day in March                  */
                }
                else
                {
                    ClkDay++;
                    newmonth = FALSE;
                }
            }
            else
            {
                ClkDay = 1;
            }
        }
        else
        {
            ClkDay = 1;
        }
    }
    else
    {
        ClkDay++;
        newmonth = FALSE;
    }
    if ( newmonth == TRUE )                      /* See if we have completed a month                   */
    {
        if ( ClkMonth >= 12 )                    /* Yes, Is this december ?                            */
        {
            ClkMonth = 1;                        /* Yes, set month to january...                       */
            ClkYear++;                           /*      ...we have a new year!                        */
        }
        else
        {
            ClkMonth++;                          /* No,  increment the month                           */
        }
    }
    ClkUpdateDOW();                              /* Compute the day of the week (i.e. Sunday ...)      */
}
#endif

/**
 * This function computes the day of the week (0 == Sunday) based on the current month, day and year.
 *
 * This function updates ClkDOW.
 *  - This function is called by ClkUpdateDate().
 */
#if CLK_DATE_EN
static  void  ClkUpdateDOW ( void )
{
    INT16U dow;


    dow = ClkDay + ClkMonthTbl[ClkMonth].MonthVal;
    if ( ClkMonth < 3 )
    {
        if ( ClkIsLeapYear ( ClkYear ) )
        {
            dow--;
        }
    }
    dow    += ClkYear + ( ClkYear / 4 );
    dow    += ( ClkYear / 400 ) - ( ClkYear / 100 );
    dow    %= 7;
    ClkDOW  = dow & 0x0f;
}
#endif

/**
 * This function is called to update the time (i.e. hours, minutes and seconds)
 *
 * @ret TRUE     if we have completed one day.
 *      FALSE    otherwise
 *  This function updates ClkSec, ClkMin and ClkHr.
 */
static  BOOLEAN  ClkUpdateTime ( void )
{
    BOOLEAN newday;


    newday = FALSE;                         /* Assume that we haven't completed one whole day yet      */
    if ( ClkSec >= 59 )                     /* See if we have completed one minute yet                 */
    {
        ClkSec = 0;                         /* Yes, clear seconds                                      */
        if ( ClkMin >= 59 )                 /*    See if we have completed one hour yet                */
        {
            ClkMin = 0;                     /*    Yes, clear minutes                                   */
            if ( ClkHr >= 23 )              /*        See if we have completed one day yet             */
            {
                ClkHr = 0;                  /*        Yes, clear hours ...                             */
                newday    = TRUE;           /*        ... change flag to indicate we have a new day    */
            }
            else
            {
                ClkHr++;                    /*        No,  increment hours                             */
            }
        }
        else
        {
            ClkMin++;                       /*    No,  increment minutes                               */
        }
    }
    else
    {
        ClkSec++;                           /* No,  increment seconds                                  */
    }
    return ( newday );
}